package spitfire.ksim.simulator;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

/**
 * <p>
 * KJob represents the job used by KSimulator in both job queue and execution
 * queue. It can be regarded as an event to drive the simulated object to take
 * some actions.
 * </p>
 * 
 * <p>
 * A job must be set a schedule time. There are two kinds of schedule scheme:
 * </p>
 * 
 * <ul>
 * <li>
 * schedule the job at UTC time stamp.<br>
 * Use this scheme by constructing the job with {@link #KJob(KSimObject, long)}</li>
 * <li>
 * schedule the job at a next local time point (without date).<br>
 * For example, schedule the job at 6:0:0 means the job will be triggered at 6
 * o'clock in the current day if current time is before 6 o'clock, or at 6
 * o'clock in the next day if the current time is after 6 o'clock. <br>
 * Use this scheme by constructing the job with
 * {@link #KJob(KSimObject, String)}.</li>
 * </ul>
 * 
 * <p>
 * To carry extra information about the job to the executor, use
 * {@link #addProperty(String, Object)} and {@link #getProperty(String)} to add
 * and retrieve properties in a String - Object map, or derive this class to add
 * more fields.
 * </p>
 * 
 * <p>
 * <b>Note that KJob is not thread safe.</b>
 * If an instance of KJob may be accessed by multiple threads, it should be 
 * synchronized externally.
 * </p>
 * 
 * @see spitfire.ksim.simulator.KSimulator
 * @see spitfire.ksim.simulator.KSimObject
 * 
 * @author Adam
 * 
 */
public class KJob {

	public static final long IMMEDIATE = -1;

	private static long nextJobId = 0;

	private final long jobId = nextJobId++;
	private long scheduleTime = IMMEDIATE;
	private String scheduleTimePoint = null;
	private KSimObject jobExecutor;
	private Map<String, Object> propertyMap = new HashMap<String, Object>();

	private Calendar c = Calendar.getInstance();
	DateFormat df = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.GERMANY);

	/**
	 * Constructs a KJob scheduled at UTC time stamp. <br/>
	 * 
	 * @param scheduleTime
	 *            - UTC time stamp of the scheduled time for this job
	 */
	public KJob(long scheduleTime) {
		this.scheduleTime = scheduleTime;
		df.setCalendar(c);	}

	/**
	 * Constructs a KJob scheduled at next local time point.<br/>
	 * 
	 * @param nextTimePoint
	 *            - a string that represents the next local time point.
	 */
	public KJob(String nextTimePoint) {
		scheduleTimePoint = nextTimePoint;
		df.setCalendar(c);
	}

	/**
	 * Returns the ID of the job.
	 * 
	 * @return ID of the job.
	 */
	public long getJobId() {
		return jobId;
	}

	/**
	 * Reset the auto-increment ID generator
	 */
	public static void resetID() {
		nextJobId = 0;
	}

	/**
	 * Convert the local next time point string to UTC time stamp according to
	 * the current simulation time of the jobExecutor
	 * 
	 * @param timePoint
	 *            - next local time point
	 */
	private long convertScheduleTime(String timePoint) {
		if (jobExecutor == null) {
			throw new RuntimeException("Job doer must be assigned first");
		}
		c.setTimeInMillis(jobExecutor.getSimTime());
		int year = c.get(Calendar.YEAR);
		int dayOfYear = c.get(Calendar.DAY_OF_YEAR);

		try {
			df.parse(timePoint).getTime();
		} catch (ParseException e) {
			e.printStackTrace();
		}
		c.set(Calendar.YEAR, year);
		c.set(Calendar.DAY_OF_YEAR, dayOfYear);
		return c.getTimeInMillis();
	}

	/**
	 * Returns the schedule time in UTC time stamp. If the job uses next local
	 * time point scheme, parse the time point string first.
	 * 
	 * @return schedule time in UTC time stamp.
	 */
	public long getScheduleTime() {
		if (scheduleTimePoint != null) {
			return convertScheduleTime(scheduleTimePoint);
		} else {
			return scheduleTime;
		}
	}

	/**
	 * Returns the executor of the job.
	 * 
	 * @return executor of the job.
	 */
	public KSimObject getJobExecutor() {
		return jobExecutor;
	}

	/**
	 * Set the executor of the job.
	 * 
	 * @param jobExecutor
	 *            - executor of the job.
	 */
	void setJobExecutor(KSimObject jobExecutor) {
		this.jobExecutor = jobExecutor;
	}

	/**
	 * Add a property to the internal built String to Object map.
	 * 
	 * @see {@link #getProperty(String)}
	 * 
	 * @param name
	 *            - name of the property.
	 * @param value
	 *            - value of the property.
	 */
	public void addProperty(String name, Object value) {
		propertyMap.put(name, value);
	}

	/**
	 * Get a property by the given name.
	 * 
	 * @see {@link #addProperty(String, Object)}
	 * 
	 * @param name
	 *            - name of the property.
	 * @return the property corresponds to the name.
	 */
	public Object getProperty(String name) {
		return propertyMap.get(name);
	}

	@Override
	public String toString() {
		final int maxLen = 5;
		StringBuilder builder = new StringBuilder();
		builder.append("KJob [jobId=");
		builder.append(jobId);
		builder.append(", jobExecutor=");
		builder.append(jobExecutor);
		builder.append(", propertyMap=");
		builder.append(propertyMap != null ? toString(propertyMap.entrySet(),
				maxLen) : null);
		builder.append(", scheduleTime=");
		builder.append(scheduleTime);
		builder.append(", scheduleTimePoint=");
		builder.append(scheduleTimePoint);
		builder.append("]");
		return builder.toString();
	}

	private String toString(Collection<?> collection, int maxLen) {
		StringBuilder builder = new StringBuilder();
		builder.append("[");
		int i = 0;
		for (Iterator<?> iterator = collection.iterator(); iterator.hasNext()
				&& i < maxLen; i++) {
			if (i > 0)
				builder.append(", ");
			builder.append(iterator.next());
		}
		builder.append("]");
		return builder.toString();
	}
}
